package com.slice.upload.service;

import com.google.common.base.Joiner;
import com.slice.upload.autoconfigure.SliceUploadProperties;
import com.slice.upload.entity.FileUploadRespond;
import com.slice.upload.strategy.FileUploadHandler;
import com.slice.upload.strategy.FileUploadPayload;
import com.slice.upload.task.FileUploadTask;
import com.slice.upload.util.ThreadUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.io.File;
import java.io.IOException;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.CompletionService;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingDeque;

import static com.slice.upload.base.RedisKey.MD5;
import static com.slice.upload.base.RedisKey.UPLOAD;
import static com.slice.upload.entity.FileUploadRespond.Code.*;

/**
 * @ClassName FileService
 * @Description
 * @Author frinder
 * @Date 2021/1/26 11:43
 * @Verion 1.0
 */
@Slf4j
public class FileService {

    @Resource
    private SliceUploadProperties properties;

    @Resource
    private RedisService redisService;

    @Resource
    private FileUploadHandler handler;

    /** 执行器 */
    private ExecutorService executor;

    /** 执行服务 */
    private CompletionService<FileUploadRespond> service;

    /**
     * 创建目录
     */
    @PostConstruct
    public void init() {
        // 执行器
        executor = ThreadUtils.getThreadPoolExecutor(properties.getMaxThreadSize());
        // 执行服务
        service = new ExecutorCompletionService<>(
                executor,
                new LinkedBlockingDeque<>(properties.getMaxQueueSize())
        );
        // 创建上传目录
        createUploadDir();
    }

    /**
     * 检查 md5
     *
     * @param payload
     * @return
     * @throws IOException
     */
    public FileUploadRespond checkFileMd5(FileUploadPayload payload) throws IOException {
        // 从 redis 中获取该 md5 文件是否已存在标识
        Object key = redisService.hget(UPLOAD.getVal(), payload.getMd5());
        if (key == null) {
            log.info("文件[{}-{}]未上传", payload.getPath(), payload.getMd5());
            // 标识不存在，则说明文件未上传过
            FileUploadRespond respond = FileUploadRespond.builder().path(payload.getPath()).code(UN_KNOWN.getCode()).build();
            return respond;
        }
        // 文件已上传，则获取处理状态
        boolean isProcessed = Boolean.parseBoolean(key.toString());
        // 获取配置文件路径
        String confFile = String.valueOf(redisService.get(MD5.getVal() + payload.getMd5()));
        return fillFileUploadRespond(payload, isProcessed, confFile);
    }

    /**
     * 上传文件
     *
     * @param payload
     * @return
     * @throws Exception
     */
    public FileUploadRespond upload(FileUploadPayload payload) throws Exception {
        if (payload.getChunk() != null && payload.getChunks() > 0) {
            // 分片上传
            return sliceUpload(payload);
        } else {
            // 单文件上传
            return singleUpload(payload);
        }
    }

    /**
     * 获取文件的 MD5 值
     *
     * @param file
     * @return
     */
    public String getFileMD5(MultipartFile file) {
        try {
            byte[] uploadBytes = file.getBytes();
            MessageDigest md5 = MessageDigest.getInstance("MD5");
            byte[] digest = md5.digest(uploadBytes);
            String result = new BigInteger(1, digest).toString(16);
            log.info("文件[{}]MD5值[{}]", file.getOriginalFilename(), result);
            return result;
        } catch (Exception e) {
            throw new RuntimeException(e.getMessage(), e);
        }
    }

    /**
     * 获取文件上传后全路径
     *
     * @param payload
     * @return
     */
    public String getFilePath(FileUploadPayload payload) {
        String uploadDir = getUploadDir(payload);
        String path = getFilePath(uploadDir, payload.getFile().getOriginalFilename());
        log.info("文件[{}]上传全路径[{}]", payload.getFile().getOriginalFilename(), path);
        return path;
    }

    /**
     * 获取文件路径
     *
     * @param args
     * @return
     */
    public String getFilePath(String... args) {
        return Joiner.on(File.separator).join(args);
    }

    /**
     * 单文件上传
     *
     * @param payload
     * @return
     * @throws IOException
     */
    private FileUploadRespond singleUpload(FileUploadPayload payload) throws Exception {
        // 获取并设置文件 md5
        String md5 = getFileMD5(payload.getFile());
        payload.setMd5(md5);
        // 获取文件上传后全路径
        String filePath = getFilePath(payload);
        // 上传文件
        File file = new File(filePath);
        payload.getFile().transferTo(file);
        // 设置已上传标识到 redis
        redisService.hset(UPLOAD.getVal(), md5, "true");
        log.info("文件[{}]上传完成，上传路径[{}]", payload.getFile().getOriginalFilename(), file.toPath());
        // 返回处理结果
        return FileUploadRespond.builder()
                .path(filePath)
                .code(ALL.getCode())
                .mtime(System.currentTimeMillis() / 1000)
                .uploadComplete(true)
                .fileId(payload.getFile().getOriginalFilename())
                .build();
    }

    /**
     * 分片上传
     *
     * @param payload
     * @return
     */
    private FileUploadRespond sliceUpload(FileUploadPayload payload) {
        try {
            // 提交分片上传任务
            service.submit(new FileUploadTask(handler, payload));
            // 获取上传结果
            FileUploadRespond respond = service.take().get();
            return respond;
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            throw new RuntimeException(e.getMessage(), e);
        }
    }

    /**
     * 填充返回文件内容信息
     *
     * @param payload
     * @param isProcessed  是否已上传完成
     * @param confFilePath 记录上传状态配置文件路径
     * @return
     * @throws IOException
     */
    private FileUploadRespond fillFileUploadRespond(FileUploadPayload payload, boolean isProcessed, String confFilePath) throws IOException {
        if (isProcessed) {
            // 文件已上传
            String uploadDirPath = getUploadDir(payload);
            log.info("文件[{}]已上传，上传目录[{}]", payload.getFile().getOriginalFilename(), uploadDirPath);
            return FileUploadRespond.builder()
                    .code(ALL.getCode())
                    .path(uploadDirPath)
                    .build();
        } else {
            log.info("文件[{}]正在上传中，请等待……", payload.getFile().getOriginalFilename());
            // 文件上传中
            File confFile = new File(confFilePath);
            byte[] completeList = FileUtils.readFileToByteArray(confFile);
            List<Integer> missChunkList = new LinkedList<>();
            for (int i = 0; i < completeList.length; i++) {
                if (completeList[i] != Byte.MAX_VALUE) {
                    missChunkList.add(i);
                }
            }
            return FileUploadRespond.builder()
                    .code(PART.getCode())
                    .missChunks(missChunkList)
                    .build();
        }
    }

    /**
     * 获取请求路径
     *
     * @param payload
     * @return
     */
    private String getUploadDir(FileUploadPayload payload) {
        String path = getFilePath(getBasePath(), payload.getPath(), payload.getMd5());
        log.info("文件[{}]上传目录[{}]", payload.getFile().getOriginalFilename(), path);
        File file = new File(path);
        if (file.exists()) {
            // 目录已存在
        } else {
            // 目录不存在
            file.mkdirs();
        }
        return path;
    }

    /**
     * 创建上传目录
     */
    private void createUploadDir() {
        // 创建目录
        String path = getBasePath();
        File file = new File(path);
        if (file.exists()) {
            // 目录已存在，不处理
        } else {
            log.info("目录[{}]不存在，创建之", file.toPath());
            file.mkdirs();
        }
    }

    /**
     * 获取全路径
     *
     * @return
     */
    private String getBasePath() {
        String path = properties.getRootDir();
        if (isWinOs()) {
            path = Joiner.on(File.separator).join(properties.getWindowDir(), properties.getRootDir());
        }
        return path;
    }

    /**
     * 判断是否为window系统
     *
     * @return
     */
    private boolean isWinOs() {
        final String winOs = "win";
        String os = System.getProperty("os.name");
        if (os.toLowerCase().startsWith(winOs)) {
            return true;
        }
        return false;
    }
}
